02 January 2019
Although it’s possible to use Shiro in any type of enterprise application, here we focus on enterprise Spring-based applications. In such these applications, JPA is commonly used to implement the data layer. This means we present users, roles, and permissions through three entity classes as follows.
@Entity @Table(name="user_tbl") public class User{ String username; String password; List<Role> roles; } @Entity @Table(name="role_tbl") public class Role{ String name; List<Permission> permissions; } @Entity @Table(name="permission_tbl") public class Permission{ String name; }
Normally, we implement an individual DAO object for each specific entity class. So, we will have UserDAO
, RoleDAO
, and PermissionDAO
classes in our project.
@Repository public interface UserDAO extends CrudRepository{ }
And normally, we implement a service class to bind dao logic to transactions.
@Service @Transactional public class UserService{ @Autowired UserDAO userDAO; public User save(User user){ return userDAO.save(user); } public User findByUsernameAndPassowrd(String username, String password){ return userDAO.findByUsernameAndPassowrd(username, password); } }
Now, we need to refactor the Realm class to utilize our service classes to authenticate/authorize users. In this case we extend org.apache.shiro.realm.jdbc.AuthorizingRealm
instead of JdbcRealm
and use our service object to provide security principals. Here is the code.
package com.ahmadsedi.shiro.web.security; import com.ahmadsedi.shiro.web.entity.Permission; import com.ahmadsedi.shiro.web.entity.User; import com.ahmadsedi.shiro.web.service.PermissionService; import com.ahmadsedi.shiro.web.service.UserService; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.HashSet; import java.util.List; import java.util.Set; /** * @author Ahmad R. Seddighi (ahmadseddighi@yahoo.com) * Date: 3/5/19 * Time: 9:22 AM */ @Component public class JpaShiroRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private PermissionService permissionService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) getAvailablePrincipal(principals); User user = userService.findByUsername(username); Set<String> roleNames = new HashSet<>(); user.getRoles().forEach(role -> roleNames.add(role.getName())); List<Permission> permissions = permissionService.findByRole(user.getRoles()); Set<String> permissionKeys = new HashSet<>(); permissions.forEach(permission -> permissionKeys.add(permission.getKey())); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames); info.setStringPermissions(permissionKeys); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken; String username = upToken.getUsername(); AuthenticationInfo info = buildAuthenticationInfo(username, userService.findByUsername(username).getPassword().toCharArray()); return info; } protected AuthenticationInfo buildAuthenticationInfo(String username, char[] password) { return new SimpleAuthenticationInfo(username, password, getName()); } public void setUserService(UserService userService) { this.userService = userService; } }
In this extended class we have to override doGetAuthenticationInfo()
, doGetAuthorizationInfo()
which authenticates a user and returns a AuthenticationInfo
and authorize a user and returns an AuthorizationInfo
object respectively.
Since we are working on a web application, we need to control web request to handle security. This means we need a web filter in our web.xml file to delegate all requests to Shiro to do all security stuff. For this purpose, Spring provides the filter DelegatingFilterProxy
in web module.
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Now, we have to configure a bean in Spring context with the same name as the above filter, which refer to ShiroFilter. The following shows our Spring configuration.
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="jpaShiroRealm" /> </bean> <!-- Configuring Apache Shiro in Spring--> <!-- Name of the bean should be same as spring filter in web.xml --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/login.html"/> <property name="successUrl" value="/home.html"/> <property name="unauthorizedUrl" value="/access-denied.html"/> <property name="filterChainDefinitions"> <value> [main] authc.loginUrl = /login.html sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager # configure properties (like session timeout) here if desired # Use the configured native session manager: securityManager.sessionManager = $sessionManager [urls] /anonymous.html = anon /protected.html = authc /authorized-with-role.html = authc, roles[admin] /authorized-with-permission.html = authc, perms["admin:admin"] </value> </property> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
That’s it; Full source code of this article can be found on my GitHub repository.